using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace Decav.Transactions
{
    /// <summary>
    /// Binds an objects properties or fields to another objects properties or fields.
    /// </summary>
    [Serializable]
    public class DataBinder : IDisposable
    {
        /// <summary>
        /// Initializes a new <see cref="DataBinder"/> to bind an object to a target.
        /// </summary>
        /// <param name="target">The object that will be the target of the bind, such
        /// as a form.</param>
        /// <param name="selector">The binding selector used to choose which properties
        /// to set on the <paramref name="target"/></param>
        public DataBinder(object target, IBindingTargetSelector selector)
	    {
            if (target == null)
                throw new ArgumentNullException("target");

            if (selector == null)
                throw new ArgumentNullException("selector");

            _targetBindSelector = selector;
            _target = target;
	    }

        private object _target;
        private object _boundObject;
        private IBoundPropertiesSelector _boundPropertiesSelector;
        private IBindingTargetSelector _targetBindSelector;
        private IDataBindEventAdapter _targetEventAdapter;
        private IDataBindEventAdapter _sourceEventAdapter;

        /// <summary>
        /// Gets the object that the binder will bind to.
        /// </summary>
        public object Target
        {
            get
            {
                return _target;
            }
        }

        /// <summary>
        /// Gets the object that the <see cref="Target"/> is bound to.
        /// </summary>
        public object BoundObject
        {
            get
            {
                return _boundObject;
            }
        }

        /// <summary>
        /// Gets or sets the adapter that listens for events on the <see cref="Target"/>
        /// and updates the members on the <see cref="BoundObject"/>.
        /// </summary>
        public IDataBindEventAdapter TargetEventAdapter
        {
            get
            {
                return _targetEventAdapter;
            }
            set
            {
                if (_boundObject != null)
                    throw new InvalidOperationException("Cannot change the SourceEventAdapter once the DataBinder is bound.");

                _targetEventAdapter = value;
            }
        }

        /// <summary>
        /// Gets or sets the adapter that listens for events on the <see cref="BoundObject"/>
        /// and updates members on the <see cref="Target"/>.
        /// </summary>
        public IDataBindEventAdapter SourceEventAdapter
        {
            get
            {
                return _sourceEventAdapter;
            }
            set
            {
                if (_boundObject != null)
                    throw new InvalidOperationException("Cannot change the SourceEventAdapter once the DataBinder is bound.");

                _sourceEventAdapter = value;
            }
        }

        /// <summary>
        /// Gets or sets the <see cref="IBindingTargetSelector"/> that will find which value
        /// to set on the <paramref name="Target"/> of the bind.
        /// </summary>
        public IBindingTargetSelector TargetBindSelector
        {
            get
            {
                return _targetBindSelector;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _targetBindSelector = value;
            }
        }

        /// <summary>
        /// Gets or sets the selector used to get the properties of the <see cref="BoundObject"/>
        /// that will be bound to the <see cref="Target"/>.
        /// </summary>
        public IBoundPropertiesSelector BoundPropertiesSelector
        {
            get
            {
                // Setup the default implementation.
                if (_boundPropertiesSelector == null)
                    _boundPropertiesSelector = new PublicPropertyBoundPropertiesSelector();

                return _boundPropertiesSelector;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _boundPropertiesSelector = value;
            }
        }

        /// <summary>
        /// Finds which <see cref="BindingTarget"/> a specific property should be bound to.
        /// </summary>
        /// <param name="targetObject">The target object that we will be binding to.</param>
        /// <param name="property">The property that is being bound onto the target object</param>
        /// <returns>The member that the <paramref name="property"/> will be bound to on the
        /// <paramref name="targetObject"/>, or null if no bind will occur for this property.</returns>
        protected BindingTarget FindBindTarget(object targetObject, BindingTarget property)
        {
            return TargetBindSelector.FindTarget(targetObject, property);
        }

        /// <summary>
        /// Gets the properties of a source object that would be bound to using the
        /// <see cref="BindingTargetSelector"/>.
        /// </summary>
        /// <param name="obj">The object to get the binding targets for.</param>
        /// <returns>An array of the binding targets that should be bound to.</returns>
        protected BindingTarget[] FindBoundProperties(object obj)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            return BoundPropertiesSelector.GetTargets(obj);
        }

        /// <summary>
        /// Binds the target's value to the source's value so that when one updates
        /// it will update the other.
        /// </summary>
        /// <param name="source">The object that is being bound.</param>
        /// <param name="target">The object that is receiving a bind.</param>
        protected virtual void BindMember(BindingTarget source, BindingTarget target)
        {
            if (SourceEventAdapter != null)
                SourceEventAdapter.Bind(source, target);

            if (TargetEventAdapter != null)
                TargetEventAdapter.Bind(target, source);
        }

        /// <summary>
        /// Binds the specified object to the <see cref="Target"/> and updates
        /// the <see cref="Target"/>'s properties to match 
        /// those of the <see cref="BoundObject"/>.
        /// </summary>
        /// <param name="objectToBind">The object to bind to.</param>
        public void Bind(object objectToBind)
        {
            Bind(objectToBind, true);
        }

        /// <summary>
        /// Binds the specified object to the <see cref="Target"/>
        /// </summary>
        /// <param name="objectToBind">The object to bind to.</param>
        /// <param name="updateTargetOnBind">Specifies if the <see cref="Target"/> should be
        /// updated when the bind occurs.</param>
        public void Bind(object objectToBind, bool updateTargetOnBind)
        {
            if (objectToBind == null)
                throw new ArgumentNullException("objectToBind");

            if (_boundObject != null)
                throw new InvalidOperationException("Cannot bind to an object when the binder is already bound to another object.");

            _boundObject = objectToBind;
            DoBind(updateTargetOnBind);
            OnBound(EventArgs.Empty);
        }

        private void DoBind(bool updateTargetOnBind)
        {
            BindingTarget[] properties = FindBoundProperties(_boundObject);

            foreach (BindingTarget property in properties)
            {
                BindingTarget target = FindBindTarget(_target, property);

                if (target != null)
                {
                    if (updateTargetOnBind)
                        target.Value = property.Value;

                    BindMember(property, target);
                }
            }
        }

        #region Events
        /// <summary>
        /// Event that is raised when the data binder is bound to an object.
        /// </summary>
        [Description("Event that is raised when the data binder is bound to an object.")]
        public event EventHandler<EventArgs> Bound;

        /// <summary>
        /// Raises the Bound event to event subscribers.
        /// </summary>
        /// <remarks>
        /// Note to inheritors: You must call the base method in order to ensure that the event is raised and clean up
        /// work is properly performed.
        /// </remarks>
        /// <param name="e">Event arguments.</param>
        protected virtual void OnBound(EventArgs e)
        {

            EventHandler<EventArgs> handlers = Bound;

            // Invoke the event handlers
            if (handlers != null)
            {
                try
                {
                    handlers(this, e);
                }   // try
                catch (Exception)
                {
                    throw;
                    // Respond to exceptions in the handler
                    // or ignore them
                }   // catch ( Exception )
            }   // if

        }
        #endregion

        #region IDisposable Members

        /// <summary>
        /// Disposes the <see cref="DataBinder"/> and the event adapters associated with it.
        /// </summary>
        public void Dispose()
        {
            if (TargetEventAdapter != null)
                TargetEventAdapter.Dispose();

            if (SourceEventAdapter != null)
                SourceEventAdapter.Dispose();
        }

        #endregion
    }
}
